/**
 * @file WebSockets.cpp
 * @date 20.05.2015
 * @author Markus Sattler
 *
 * Copyright (c) 2015 Markus Sattler. All rights reserved.
 * This file is part of the WebSockets for Arduino.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include "WebSockets.h"

#ifdef ESP8266
#include <core_esp8266_features.h>
#endif

extern "C" {
#ifdef CORE_HAS_LIBB64
#include <libb64/cencode.h>
#else
#include "libb64/cencode_inc.h"
#endif
}

#ifdef ESP8266
#include <Hash.h>
#else

extern "C" {
#include "libsha1/libsha1.h"
}

#endif

/**
 *
 * @param client WSclient_t *  ptr to the client struct
 * @param code uint16_t see RFC
 * @param reason
 * @param reasonLen
 */
void WebSockets::clientDisconnect(WSclient_t * client, uint16_t code, char * reason, size_t reasonLen) {
    DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] clientDisconnect code: %u\n", client->num, code);
    if(client->status == WSC_CONNECTED && code) {
        if(reason) {
            sendFrame(client, WSop_close, (uint8_t *) reason, reasonLen);
        } else {
            uint8_t buffer[2];
            buffer[0] = ((code >> 8) & 0xFF);
            buffer[1] = (code & 0xFF);
            sendFrame(client, WSop_close, &buffer[0], 2);
        }
    }
    clientDisconnect(client);
}

/**
 *
 * @param client WSclient_t *   ptr to the client struct
 * @param opcode WSopcode_t
 * @param payload uint8_t *
 * @param length size_t
 * @param mask bool             add dummy mask to the frame (needed for web browser)
 * @param fin bool              can be used to send data in more then one frame (set fin on the last frame)
 * @param headerToPayload bool  set true if the payload has reserved 14 Byte at the beginning to dynamically add the Header (payload neet to be in RAM!)
 * @return true if ok
 */
bool WebSockets::sendFrame(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool mask, bool fin, bool headerToPayload) {

    if(client->tcp && !client->tcp->connected()) {
        DEBUG_WEBSOCKETS("[WS][%d][sendFrame] not Connected!?\n", client->num);
        return false;
    }

    if(client->status != WSC_CONNECTED) {
        DEBUG_WEBSOCKETS("[WS][%d][sendFrame] not in WSC_CONNECTED state!?\n", client->num);
        return false;
    }

    DEBUG_WEBSOCKETS("[WS][%d][sendFrame] ------- send massage frame -------\n", client->num);
    DEBUG_WEBSOCKETS("[WS][%d][sendFrame] fin: %u opCode: %u mask: %u length: %u headerToPayload: %u\n", client->num, fin, opcode, mask, length, headerToPayload);

    if(opcode == WSop_text) {
        DEBUG_WEBSOCKETS("[WS][%d][sendFrame] text: %s\n", client->num, (payload + (headerToPayload ? 14 : 0)));
    }

    uint8_t maskKey[4] = { 0x00, 0x00, 0x00, 0x00 };
    uint8_t buffer[WEBSOCKETS_MAX_HEADER_SIZE] = { 0 };

    uint8_t headerSize;
    uint8_t * headerPtr;
    uint8_t * payloadPtr = payload;
    bool useInternBuffer = false;
    bool ret = true;

    // calculate header Size
    if(length < 126) {
        headerSize = 2;
    } else if(length < 0xFFFF) {
        headerSize = 4;
    } else {
        headerSize = 10;
    }

    if(mask) {
        headerSize += 4;
    }

#ifdef WEBSOCKETS_USE_BIG_MEM
    // only for ESP since AVR has less HEAP
    // try to send data in one TCP package (only if some free Heap is there)
    if(!headerToPayload && ((length > 0) && (length < 1400)) && (GET_FREE_HEAP > 6000)) {
        DEBUG_WEBSOCKETS("[WS][%d][sendFrame] pack to one TCP package...\n", client->num);
        uint8_t * dataPtr = (uint8_t *) malloc(length + WEBSOCKETS_MAX_HEADER_SIZE);
        if(dataPtr) {
            memcpy((dataPtr + WEBSOCKETS_MAX_HEADER_SIZE), payload, length);
            headerToPayload = true;
            useInternBuffer = true;
            payloadPtr = dataPtr;
        }
    }
#endif

    // set Header Pointer
    if(headerToPayload) {
        // calculate offset in payload
        headerPtr = (payloadPtr + (WEBSOCKETS_MAX_HEADER_SIZE - headerSize));
    } else {
        headerPtr = &buffer[0];
    }

    // create header

    // byte 0
    *headerPtr = 0x00;
    if(fin) {
        *headerPtr |= bit(7);    ///< set Fin
    }
    *headerPtr |= opcode;        ///< set opcode
    headerPtr++;

    // byte 1
    *headerPtr = 0x00;
    if(mask) {
        *headerPtr |= bit(7);    ///< set mask
    }

    if(length < 126) {
        *headerPtr |= length;
        headerPtr++;
    } else if(length < 0xFFFF) {
        *headerPtr |= 126;
        headerPtr++;
        *headerPtr = ((length >> 8) & 0xFF);
        headerPtr++;
        *headerPtr = (length & 0xFF);
        headerPtr++;
    } else {
        // Normally we never get here (to less memory)
        *headerPtr |= 127;
        headerPtr++;
        *headerPtr = 0x00;
        headerPtr++;
        *headerPtr = 0x00;
        headerPtr++;
        *headerPtr = 0x00;
        headerPtr++;
        *headerPtr = 0x00;
        headerPtr++;
        *headerPtr = ((length >> 24) & 0xFF);
        headerPtr++;
        *headerPtr = ((length >> 16) & 0xFF);
        headerPtr++;
        *headerPtr = ((length >> 8) & 0xFF);
        headerPtr++;
        *headerPtr = (length & 0xFF);
        headerPtr++;
    }

    if(mask) {
        if(useInternBuffer) {
            // if we use a Intern Buffer we can modify the data
            // by this fact its possible the do the masking
            for(uint8_t x = 0; x < sizeof(maskKey); x++) {
                maskKey[x] = random(0xFF);
                *headerPtr = maskKey[x];
                headerPtr++;
            }

            uint8_t * dataMaskPtr;

            if(headerToPayload) {
                dataMaskPtr = (payloadPtr + WEBSOCKETS_MAX_HEADER_SIZE);
            } else {
                dataMaskPtr = payloadPtr;
            }

            for(size_t x = 0; x < length; x++) {
                dataMaskPtr[x] = (dataMaskPtr[x] ^ maskKey[x % 4]);
            }

        } else {
            *headerPtr = maskKey[0];
            headerPtr++;
            *headerPtr = maskKey[1];
            headerPtr++;
            *headerPtr = maskKey[2];
            headerPtr++;
            *headerPtr = maskKey[3];
            headerPtr++;
        }
    }

#ifndef NODEBUG_WEBSOCKETS
    unsigned long start = micros();
#endif

    if(headerToPayload) {
        // header has be added to payload
        // payload is forced to reserved 14 Byte but we may not need all based on the length and mask settings
        // offset in payload is calculatetd 14 - headerSize
        if(client->tcp->write(&payloadPtr[(WEBSOCKETS_MAX_HEADER_SIZE - headerSize)], (length + headerSize)) != (length + headerSize)) {
            ret = false;
        }
    } else {
        // send header
        if(client->tcp->write(&buffer[0], headerSize) != headerSize) {
            ret = false;
        }

        if(payloadPtr && length > 0) {
            // send payload
            if(client->tcp->write(&payloadPtr[0], length) != length) {
                ret = false;
            }
        }
    }

    DEBUG_WEBSOCKETS("[WS][%d][sendFrame] sending Frame Done (%uus).\n", client->num, (micros() - start));

#ifdef WEBSOCKETS_USE_BIG_MEM
    if(useInternBuffer && payloadPtr) {
        free(payloadPtr);
    }
#endif

    return ret;
}

/**
 * callen when HTTP header is done
 * @param client WSclient_t *  ptr to the client struct
 */
void WebSockets::headerDone(WSclient_t * client) {
    client->status = WSC_CONNECTED;
    client->cWsRXsize = 0;
    DEBUG_WEBSOCKETS("[WS][%d][headerDone] Header Handling Done (%uus).\n", client->num);
#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC)
    client->cHttpLine = "";
    handleWebsocket(client);
#endif
}

/**
 * handle the WebSocket stream
 * @param client WSclient_t *  ptr to the client struct
 */
void WebSockets::handleWebsocket(WSclient_t * client) {
    if(client->cWsRXsize == 0) {
        handleWebsocketCb(client);
    }
}

/**
 * wait for
 * @param client
 * @param size
 */
bool WebSockets::handleWebsocketWaitFor(WSclient_t * client, size_t size) {
    if(!client->tcp || !client->tcp->connected()) {
        return false;
    }

    if(size > WEBSOCKETS_MAX_HEADER_SIZE) {
        DEBUG_WEBSOCKETS("[WS][%d][handleWebsocketWaitFor] size: %d too big!\n", client->num, size);
        return false;
    }

    if(client->cWsRXsize >= size) {
        return true;
    }

    DEBUG_WEBSOCKETS("[WS][%d][handleWebsocketWaitFor] size: %d cWsRXsize: %d\n", client->num, size, client->cWsRXsize);
    readCb(client, &client->cWsHeader[client->cWsRXsize], (size - client->cWsRXsize), std::bind([](WebSockets * server, size_t size, WSclient_t * client, bool ok) {
        DEBUG_WEBSOCKETS("[WS][%d][handleWebsocketWaitFor][readCb] size: %d ok: %d\n", client->num, size, ok);
        if(ok) {
            client->cWsRXsize = size;
            server->handleWebsocketCb(client);
        } else {
            DEBUG_WEBSOCKETS("[WS][%d][readCb] failed.\n", client->num);
            client->cWsRXsize = 0;
            // timeout or error
            server->clientDisconnect(client, 1002);
        }
    }, this, size, std::placeholders::_1, std::placeholders::_2));
    return false;
}

void WebSockets::handleWebsocketCb(WSclient_t * client) {

    if(!client->tcp || !client->tcp->connected()) {
        return;
    }

    uint8_t * buffer = client->cWsHeader;

    WSMessageHeader_t * header = &client->cWsHeaderDecode;
    uint8_t * payload = NULL;

    uint8_t headerLen = 2;

    if(!handleWebsocketWaitFor(client, headerLen)) {
        return;
    }

    // split first 2 bytes in the data
    header->fin = ((*buffer >> 7) & 0x01);
    header->rsv1 = ((*buffer >> 6) & 0x01);
    header->rsv2 = ((*buffer >> 5) & 0x01);
    header->rsv3 = ((*buffer >> 4) & 0x01);
    header->opCode = (WSopcode_t) (*buffer & 0x0F);
    buffer++;

    header->mask = ((*buffer >> 7) & 0x01);
    header->payloadLen = (WSopcode_t) (*buffer & 0x7F);
    buffer++;

    if(header->payloadLen == 126) {
        headerLen += 2;
        if(!handleWebsocketWaitFor(client, headerLen)) {
            return;
        }
        header->payloadLen = buffer[0] << 8 | buffer[1];
        buffer += 2;
    } else if(header->payloadLen == 127) {
        headerLen += 8;
        // read 64bit integer as length
        if(!handleWebsocketWaitFor(client, headerLen)) {
            return;
        }

        if(buffer[0] != 0 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) {
            // really too big!
            header->payloadLen = 0xFFFFFFFF;
        } else {
            header->payloadLen = buffer[4] << 24 | buffer[5] << 16 | buffer[6] << 8 | buffer[7];
        }
        buffer += 8;
    }

    DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] ------- read massage frame -------\n", client->num);
    DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] fin: %u rsv1: %u rsv2: %u rsv3 %u  opCode: %u\n", client->num, header->fin, header->rsv1, header->rsv2, header->rsv3, header->opCode);
    DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] mask: %u payloadLen: %u\n", client->num, header->mask, header->payloadLen);

    if(header->payloadLen > WEBSOCKETS_MAX_DATA_SIZE) {
        DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] payload too big! (%u)\n", client->num, header->payloadLen);
        clientDisconnect(client, 1009);
        return;
    }

    if(header->mask) {
        headerLen += 4;
        if(!handleWebsocketWaitFor(client, headerLen)) {
            return;
        }
        header->maskKey = buffer;
        buffer += 4;
    }

    if(header->payloadLen > 0) {
        // if text data we need one more
        payload = (uint8_t *) malloc(header->payloadLen + 1);

        if(!payload) {
            DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] to less memory to handle payload %d!\n", client->num, header->payloadLen);
            clientDisconnect(client, 1011);
            return;
        }
        readCb(client, payload, header->payloadLen, std::bind(&WebSockets::handleWebsocketPayloadCb, this, std::placeholders::_1, std::placeholders::_2, payload));
    } else {
        handleWebsocketPayloadCb(client, true, NULL);
    }
}

void WebSockets::handleWebsocketPayloadCb(WSclient_t * client, bool ok, uint8_t * payload) {

    WSMessageHeader_t * header = &client->cWsHeaderDecode;
    if(ok) {
        if(header->payloadLen > 0) {
            payload[header->payloadLen] = 0x00;

            if(header->mask) {
                //decode XOR
                for(size_t i = 0; i < header->payloadLen; i++) {
                    payload[i] = (payload[i] ^ header->maskKey[i % 4]);
                }
            }
        }

        switch(header->opCode) {
            case WSop_text:
                DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] text: %s\n", client->num, payload);
                // no break here!
            case WSop_binary:
            case WSop_continuation:
                messageReceived(client, header->opCode, payload, header->payloadLen, header->fin);
                break;
            case WSop_ping:
                // send pong back
                sendFrame(client, WSop_pong, payload, header->payloadLen, true);
                break;
            case WSop_pong:
                DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] get pong (%s)\n", client->num, payload ? (const char*)payload : "");
                break;
            case WSop_close: {
                uint16_t reasonCode = 1000;
                if(header->payloadLen >= 2) {
                    reasonCode = payload[0] << 8 | payload[1];
                }

                DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] get ask for close. Code: %d", client->num, reasonCode);
                if(header->payloadLen > 2) {
                    DEBUG_WEBSOCKETS(" (%s)\n", (payload + 2));
                } else {
                    DEBUG_WEBSOCKETS("\n");
                }
                clientDisconnect(client, 1000);
            }
            	break;
            default:
                clientDisconnect(client, 1002);
                break;
        }

        if(payload) {
            free(payload);
        }

        // reset input
        client->cWsRXsize = 0;
#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC)
        //register callback for next message
        handleWebsocketWaitFor(client, 2);
#endif

    } else {
        DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] missing data!\n", client->num);
        free(payload);
        clientDisconnect(client, 1002);
    }
}

/**
 * generate the key for Sec-WebSocket-Accept
 * @param clientKey String
 * @return String Accept Key
 */
String WebSockets::acceptKey(String & clientKey) {
    uint8_t sha1HashBin[20] = { 0 };
#ifdef ESP8266
    sha1(clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", &sha1HashBin[0]);
#else
    clientKey += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
    SHA1_CTX ctx;
    SHA1Init(&ctx);
    SHA1Update(&ctx, (const unsigned char*)clientKey.c_str(), clientKey.length());
    SHA1Final(&sha1HashBin[0], &ctx);
#endif

    String key = base64_encode(sha1HashBin, 20);
    key.trim();

    return key;
}

/**
 * base64_encode
 * @param data uint8_t *
 * @param length size_t
 * @return base64 encoded String
 */
String WebSockets::base64_encode(uint8_t * data, size_t length) {
    size_t size = ((length * 1.6f) + 1);
    char * buffer = (char *) malloc(size);
    if(buffer) {
        base64_encodestate _state;
        base64_init_encodestate(&_state);
        int len = base64_encode_block((const char *) &data[0], length, &buffer[0], &_state);
        len = base64_encode_blockend((buffer + len), &_state);

        String base64 = String(buffer);
        free(buffer);
        return base64;
    }
    return String("-FAIL-");
}

/**
 * read x byte from tcp or get timeout
 * @param client WSclient_t *
 * @param out  uint8_t * data buffer
 * @param n size_t byte count
 * @return true if ok
 */
bool WebSockets::readCb(WSclient_t * client, uint8_t * out, size_t n, WSreadWaitCb cb) {
#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC)
    if(!client->tcp || !client->tcp->connected()) {
        return false;
    }

    client->tcp->readBytes(out, n, std::bind([](WSclient_t * client, bool ok, WSreadWaitCb cb) {
        if(cb) {
            cb(client, ok);
        }
    }, client, std::placeholders::_1, cb));

#else
    unsigned long t = millis();
    size_t len;
    DEBUG_WEBSOCKETS("[readCb] n: %d t: %d\n", n, t);
    while(n > 0) {
        if(client->tcp == NULL) {
            DEBUG_WEBSOCKETS("[readCb] tcp is null!\n");
            if(cb) {
                cb(client, false);
            }
            return false;
        }

        if(!client->tcp->connected()) {
            DEBUG_WEBSOCKETS("[readCb] not connected!\n");
            if(cb) {
                cb(client, false);
            }
            return false;
        }

        if((millis() - t) > WEBSOCKETS_TCP_TIMEOUT) {
            DEBUG_WEBSOCKETS("[readCb] receive TIMEOUT! %d\n", (millis() - t));
            if(cb) {
                cb(client, false);
            }
            return false;
        }

        if(!client->tcp->available()) {
#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266)
            delay(0);
#endif
            continue;
        }

        len = client->tcp->read((uint8_t*) out, n);
        if(len) {
            t = millis();
            out += len;
            n -= len;
            //DEBUG_WEBSOCKETS("Receive %d left %d!\n", len, n);
        } else {
            //DEBUG_WEBSOCKETS("Receive %d left %d!\n", len, n);
        }
#if (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266)
        delay(0);
#endif
    }
    if(cb) {
        cb(client, true);
    }
#endif
    return true;
}
